Skip to content
This repository was archived by the owner on Feb 5, 2025. It is now read-only.

Conversation

@mokagio
Copy link
Contributor

@mokagio mokagio commented Dec 8, 2022

Adds objects to model OAuth token request and response, the pre-requisite Proof of Code Encryption Key object for the request that generates the URL to use for the token, and the logic to generate the token request URL.

@crazytonyli one disadvantage of using an incremental approach to building a big feature such as this is that the first PRs fail to tell the story of the system being developed, because they are only individual components not yet used together.

Is this format acceptable for you to review? Otherwise I could:

  • Rework my WIP code to create a new PR that adds the higher level consumer of all these pieces (working name NewGoogleAuthenticator) so that we can do PRs that add a few bits and also show how they are used; or
  • Dump what I have in a >800 lines PR, after which it might be easier to move adding little bits to the existing end-to-end setup (eg. more error handling, MFA, etc...)
  • Keep working like this and share another maybe 2-3 PRs before we get to a working end-to-end bare bone setup

Let me know what would work best for you. Thanks!

To test

If CI is green, we're good.

See also


  • I have considered if this change warrants release notes and have added them to the appropriate section in the CHANGELOG.md if necessary.

Comment on lines +16 to +27
// TODO: We might want to add some of these or them configurable
//
// The request we make with the SDK asks for:
//
// - email
// - profile
// - https://www.googleapis.com/auth/userinfo.email
// - https://www.googleapis.com/auth/userinfo.profile
// - openid
//
// See https://developers.google.com/identity/protocols/oauth2/scopes
("scope", "https://www.googleapis.com/auth/userinfo.email")
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Spoiler: I updated this in my WIP branch but haven't seen any difference in the behavior, but that's likely due to my implementation being quite minimal at this point. Or, it's possible that only the backend would notice the difference, when completing the authentication using the OAuth token.

Comment on lines 34 to 41
guard var components = URLComponents(url: baseURL, resolvingAgainstBaseURL: false) else {
throw URLError(
.unsupportedURL,
userInfo: [
NSLocalizedDescriptionKey: "Could not create `URLComponents` instance from \(baseURL)"
]
)
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you think this is acceptable as a URLError, or should I update it to another OAuthError case for consistency?

I'm leaning toward moving it to OAuthError.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's safe to ! here, since we already ! the googleSignInBaseURL?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a great observation! After all, what are the chances URLComponents will not initialize with a valid input URL?

Copy link
Contributor Author

@mokagio mokagio Jan 5, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in d749c80

Comment on lines +12 to +13
/// The type of token returned. At this time, this field's value is always set to Bearer.
let tokenType: String
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The documentation comment I lifted from from the Google docs makes me wonder whether there's even a point in storing it at this time?

//
// A code_verifier is a high-entropy cryptographic random string using the unreserved
// characters [A-Z] / [a-z] / [0-9] / "-" / "." / "_" / "~", with a minimum length of 43
// characters and a maximum length of 128 characters.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I plan to address the completion of this type in a dedicated PR. The process works for development purposes in the current, plain mode.

Comment on lines +13 to +15
func testCodeChallengeInS256ModeIsEncodedAsPerSpec() {
// TODO:
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I put this down as a double reminder to finish the PKCE implementation. See also previous comment.

@crazytonyli
Copy link
Contributor

@mokagio I'm okay with how ever you'd like to break down the end-to-end flow. I don't see any difficulties reviewing individual component, low-level or UI. 😄

// error, which we would catch because the unit tests would crash.
static var googleSignInBaseURL = URL(string: "https://accounts.google.com/o/oauth2/v2/auth")!

static func googleSignInAuthURL(clientId: String, pkce: ProofKeyForCodeExchange) throws -> URL {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we are essentially implement OAuth 2, I think we can replace "Google Sign In" with a more generic term? Like, this function becomes oauth2AuthenticationURL, what do you think?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm making assumption here, but I imagine the whole Google Sign In workflow can be used in "Sign In via Facebook", or even the WordPress OAuth2.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. I was thinking about this before going AFK and realized I may be using unclear boundaries here.

You might have noticed I made an OAuth folder and a GoogleSignIn one. My intent was to separate the components that are GoogleSignIn specific from those that are part of the OAuth spec. One issue with how I executed this is that I haven't been cross checking between the OAuth and Google documentations so there might be stuff in the OAuth folder that's still Google specific, and vice versa.

Thinking about it since, I came to the conclusion that I'd prefer to separate OAuth- from Google- specific code only once we'll need to add a new OAuth client (likely WordPress OAuth2). As such, I'm tempted to follow up this PR with one that moves all the OAuth/ code into GoogleSignIn/, renaming some of the types if necessary.

My rationale for the suggestion above is that I'm more interested in having a working version of the SDK-less authentication than a general purpose OAuth client, even though building a client first might make it easier to add further authentication implementation in the future.

What do you think?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good to me. We probably don't need to "pre-abstract" stuff if there is no immediate plan to support another OAuth provider (service? server?). I trust you'll strike the right balance 👍

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I trust you'll strike the right balance 👍

Thanks for the confidence.

Comment on lines 34 to 41
guard var components = URLComponents(url: baseURL, resolvingAgainstBaseURL: false) else {
throw URLError(
.unsupportedURL,
userInfo: [
NSLocalizedDescriptionKey: "Could not create `URLComponents` instance from \(baseURL)"
]
)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's safe to ! here, since we already ! the googleSignInBaseURL?

}

components.queryItems = queryItems
return try components.asURL()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's alsy safe to ! here too?

Also, this asURL is an Alamofire function, maybe we shouldn't use it here since it throws an AFError?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another good point. Addressed in d5c6fd0

var components = URLComponents()
components.queryItems = items

guard let query = components.query else {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it's possible to have a nil query in this case?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. That should be the case.

🤔 I guess I'm just uncomfortable force unwrapping stuff, but it does make for simpler code in a case like this one.

I tried to write a test that resulted in this code throwing, but I wasn't able to. That's not to say that there is no way for the failure to occur, of course. Still, I feel a bit more confident about removing the errors here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 8b6f6a8

throw OAuthError.failedToBuildURLQuery
}

guard let data = query.data(using: .utf8) else {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it's possible to fail to utf8 encode the query string?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 8b6f6a8 while working on your previous comment.

Thanks!

// The code verifier should have enough entropy to make it impractical to guess the value.
struct ProofKeyForCodeExchange {

enum Mode {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not good at naming things, but what do you think about renaming this to Method and the var method to something like var googleOAuthParameterName?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for raising this. Method would definitely be a better name, as that's what the spec uses 👍

Addressed in 8abbb29.

As for the computed variable name, I went for urlQueryParameterValue to try and make it clearer where to use it.

let codeVerifier: String
let mode: Mode

init(codeVerifier: String, mode: Mode) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Considering we should randomly generate codeVerifier, do you think we can make this argument nil by default and generate a random string internally?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the suggestion. I'm keen on generating the string internally according to the spec but I think that's work that could live in a dedicated PR. Does that sound okay?

What I have in mind is along the lines of your suggestion, with the difference of providing the random string as the default value.

init(codeVerifier: String = makeRandomPKCEString(), method: Method = .s256) {

@mokagio mokagio force-pushed the mokagio/oauth-part-2-of-n branch from 63a01aa to d749c80 Compare January 5, 2023 02:46
This also resulted in renaming the computed variable `method` to
`testMethodURLQueryParameterValue` because `Method.method` would have
been confusing. The new name also better reflects where the value is
meant to be used.
@mokagio mokagio requested a review from crazytonyli January 5, 2023 03:45
@mokagio mokagio marked this pull request as ready for review January 18, 2023 03:40
@mokagio mokagio merged commit d170cbd into trunk Jan 18, 2023
@mokagio mokagio deleted the mokagio/oauth-part-2-of-n branch January 18, 2023 03:41
@mokagio mokagio mentioned this pull request Jan 25, 2023
1 task
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants